﻿#region Copyright 2012 by Roger Knapp, Licensed under the Apache License, Version 2.0
/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;

namespace CsProjectProjector
{
    class ProjectConverter
    {
        public static void ConvertProject(string projpath, string[] templates)
        {
            foreach (string template in templates)
            {
                string newExtension = "." + Path.GetFileName(template);

                var proj = new ProjectConverter(projpath);
                proj.ConvertProject(XElement.Load(template), template);
                //Only for solutions -> proj.MapProjectReferences(newExtension);
                proj.Save(Path.ChangeExtension(projpath, newExtension));
            }
        }

        private static readonly Regex MakefileMacro = new Regex(@"\$\((?<field>[\w-_\.]*)(?<replace>(?:\:(?<name>[^:=\)]+)=(?<value>[^:\)]*))+)?\)");
        private readonly string _projpath;
        private readonly XElement _proj;
        private int _index;
        private Dictionary<XName, bool> _hasRemoved;

        public ProjectConverter(string projpath)
        {
            _hasRemoved = new Dictionary<XName, bool>();
            _projpath = projpath;
            _proj = XElement.Load(_projpath);
            _index = 0;
        }

        public int ToolsVersion
        {
            get { return (int)(10 * double.Parse(_proj.AttributeByName("ToolsVersion").Value)); }
        }

        public void Save(string path)
        {
            var settings =
                new XmlWriterSettings
                {
                    Encoding = new UTF8Encoding(true),
                    OmitXmlDeclaration = true,
                    Indent = true,
                    IndentChars = "  ",
                    NewLineChars = Environment.NewLine,
                    ConformanceLevel = ConformanceLevel.Document,
                    CheckCharacters = false
                };

            using (var io = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None))
            using (var xml = XmlWriter.Create(io, settings))
                _proj.Save(xml);
        }

        public void MapProjectReferences(string newExtension)
        {
            foreach (var item in _proj.ElementsByName("ItemGroup")
                .SelectMany(e => e.ElementsByName("ProjectReference")))
            {
                var attr = item.AttributeByName("Include");
                attr.Value = Path.ChangeExtension(attr.Value, newExtension);
            }
        }

        public void ConvertProject(XElement template, string templatePath)
        {
            // 1. Merge/Overwrite all root attributes...
            foreach (var attr in template.Attributes())
                _proj.SetAttributeValue(attr.Name, attr.Value);

            var elements = template.Elements().Where(EvaluatePrecondition).ToList();
            for (int ix = 0; ix < elements.Count; ix++)
            {
                var group = elements[ix];
                if (group.Name.Namespace.NamespaceName == "urn:schemas-csharp-project:template")
                {
                    if (group.Name.LocalName == "Import")
                    {
                        XElement include = XElement.Load(
                            Path.Combine(Path.GetDirectoryName(templatePath), group.AttributeByName("Project").Value)
                            );
                        elements.InsertRange(ix + 1, include.Elements());
                    }
                    elements.RemoveAt(ix--);
                }
            }

            // 2. Merge/Insert PropertyGroup, ItemGroup, etc
            foreach (var group in elements)
            {
                switch (group.Name.LocalName)
                {
                    case "PropertyGroup":
                        MergePropertyGroup(group);
                        break;
                    case "ItemGroup":
                        MergeItemGroup(group);
                        break;
                    default:
                        MergeOther(group);
                        break;
                }
            }
        }

        bool EvaluatePrecondition(XElement element)
        {
            if (element.HasAttributeByName("Precondition") &&
                   element.AttributeByName("Precondition").Name.Namespace.NamespaceName == "urn:schemas-csharp-project:template")
            {
                string condition = element.AttributeByName("Precondition").Value;
                element.AttributeByName("Precondition").Remove();
                condition = MakefileMacro.Replace(condition, m => ReplaceVariable(null, m));

                int opix = condition.IndexOf("==");
                opix = opix >= 0 ? opix : condition.IndexOf("!=");
                if (opix < 0)
                    throw new ArgumentException("Invalid precodition = " + condition);

                string op = condition.Substring(opix, 2);
                string lhs = condition.Substring(0, opix).Trim();
                string rhs = condition.Substring(opix + 2).Trim();

                int compare = StringComparer.OrdinalIgnoreCase.Compare(lhs, rhs);
                switch (op)
                {
                    case "==": if (compare != 0) return false; else break;
                    case "!=": if (compare == 0) return false; else break;
                }
            }
            return true;
        }

        IEnumerable<XElement> MatchingCondition(IEnumerable<XElement> source, XAttribute cond)
        {
            if (cond == null)
            {
                return source.Where(
                    n => n.Attributes().FirstOrDefault(a => a.Name == "Condition") == null);
            }
            else
            {
                return source.Where(
                    n => n.Attribute(cond.Name) != null
                         && n.Attribute(cond.Name).Value.Replace(" ", "") == cond.Value.Replace(" ", ""));
            }
        }

        private void MergeGroupBy(XElement group, 
            Func<XElement, XElement, bool> compareGroups, 
            Func<XElement, XElement, bool> compareItems,
            Func<XElement, XElement, XElement> mergeValues)
        {
            XElement mergeWith = MatchingCondition(
                _proj.Elements(group.Name).Where(t => compareGroups(group, t)), 
                group.Attribute("Condition")
                ).FirstOrDefault();

            if (mergeWith == null)
            {
                _proj.InsertAt(_index++, group);
            }
            else
            {
                _index = 1 + _proj.IndexOf(mergeWith);
                foreach (XElement e in group.Elements())
                {
                    var existing = mergeWith.Elements(e.Name).FirstOrDefault(f => compareItems(e, f));
                    if (existing != null)
                        existing.ReplaceWith(mergeValues(existing, e));
                    else
                        mergeWith.Add(mergeValues(existing, e));
                }
            }
        }

        private void MergePropertyGroup(XElement group)
        {
            MergeGroupBy(group, (e, f) => true, (e, f) => true, MergeProperty);
        }

        private XElement MergeProperty(XElement existing, XElement value)
        {
            string text = value.Value;
            string transformed = MakefileMacro.Replace(text, m => ReplaceVariable(existing, m));
            if (transformed != text)
                value.SetValue(transformed);
            return value;
        }

        private string ReplaceVariable(XElement existing, Match m)
        {
            string value = m.Value;
            string fld = m.Groups["field"].Value;

            var item = existing == null ? null : existing.Parent.ElementByName(fld);
            if (item == null)
                item = MatchingCondition(_proj.ElementsByName("PropertyGroup"), null)
                    .SelectMany(p => p.ElementsByName(fld))
                    .FirstOrDefault();
            if (item != null)
                value = item.Value;

            if (value != null && m.Groups["replace"].Success)
            {
                for (int i = 0; i < m.Groups["replace"].Captures.Count; i++)
                {
                    string replace = m.Groups["name"].Captures[i].Value;
                    string with = m.Groups["value"].Captures[i].Value;
                    value = value.Replace(replace, with);
                }
            }
            return value;
        }

        private void MergeItemGroup(XElement group)
        {
            string aname = "Include";
            MergeGroupBy(group,
                (a, b) =>   a.FirstElementName() == b.FirstElementName(),
                (a, b) =>
                            a.HasAttributeByName(aname) && b.HasAttributeByName(aname)
                            && a.AttributeByName(aname).Value == b.AttributeByName(aname).Value,
                (existing, value) => value
            );
        }

        private void MergeOther(XElement element)
        {
            if (!_hasRemoved.ContainsKey(element.Name))
            {
                _hasRemoved.Add(element.Name, true);
                
                var first = _proj.Elements(element.Name).FirstOrDefault();
                if(first != null)
                    _index = _proj.IndexOf(first);

                _proj.Elements(element.Name).Remove();
            }

            _proj.InsertAt(_index++, element);
        }
    }
}
